Tema 02 - Limpieza y Tratamiento de datos

Técnicas para ‘Big Data’ en Economía - Curso 2025/26
Universidad de Alicante

Pedro Albarrán

Dpto. de Fundamentos del Análisis Económico. Universidad de Alicante

Introduccion. Datos ordenados

Limpieza y “doma” de datos

  • Un análisis de datos adecuado requiere (mucho) tiempo de trabajo “sucio”

  • tidyverse incluye una colección de bibliotecas con herramientes eficientes para el proceso de “tratamiento de datos” (“data wrangling”)

  • El objetivo es tener un conjunto de datos ordenado y limpio para poder realizar análisis de manera eficiente

  • Esto puede requerir seleccionar columnas, filtrar filas, crear nuevas variables, ordenar, agrupar, resumir, etc.

Datos ordenados

¿Qué son datos ordenados (‘tidy data’)?

1.- Cada columna es una variable: mide el mismo atributo entre unidades

2.- Cada fila es una observación (caso): misma unidad a través de atributos

3.- Cada celda es un valor

  • Tenemos información similar y no redundante en una misma tabla
  • Es una forma natural (variable = vector columna) para trabajar con datos
  • tidyverse es eficiente con datos ordenados

Datos no ordenados

  • Otras estructuras como esta pueden tener sentido para mostrar información (o por convenciones)
  • La visualización es atractiva, PERO sobran filas para analizar los datos: ej., total de personas con hijos y sin pareja entre 30 y 39 años

Transformación de datos (una tabla)

Funciones de transformación de datos

  • La mayoría de operaciones pueden realizarse combinando 5 “verbos”

    • NOTA: existe una colección de “chuletas” de R, p.e., para transformación.
  • Todos tienen como primer argumento un data frame, los siguientes describen qué hacer (con columnas o filas) y devuelven otro data frame

1.- select(): selecciona variables por nombres o posiciones de columnas, separados por comas

select(presidential, name, party)
select(presidential, 1:2, 4)

Filtrar filas

2.- filter(): conserva filas en las que la condición lógica es verdadera

filter(presidential, party == "Republican")
filter(presidential, start > 1973 & party == "Democratic")
  • Se pueden combinar (anidar) porque ambas toman y devuelve un data frame, pero así son difíciles de leer
select(filter(presidential, start > 1973), name)

El operador de tubería %>%

  • datos %>% filter(condition) equivale a filter(datos, condition)

    • Aplicable a cualquier función: 10 %>% log() es log(10)
  • El anidamiento es fácil:

    • Tomar presidential y pasarlo a filtrar (produce un nuevo data frame);
    • Tomar este resultado y pasarlo a seleccionar.
presidential %>% 
  filter(start > 1973) %>% 
  select(name)
  • Atajo de teclado: Cmd / Ctrl + Mays + M

  • También existe una tubería en R base: |>

Crear nuevas variables

3.-mutate(): añade nuevas columnas, creando variables según una fórmula a partir de otras

  • también rename(): cambiar el nombre de una columna

# evitar "machacar" la fuente original
mypresidents <- presidential %>%          
                  mutate(duracion = end - start) 

# crear varias, separadas por coma
presidential %>% mutate(sigloXXI = start > 2000,   
                        duracion = end - start,    
                        duracio2 = duracion*2   )  

presidential %>% rename(nombre = name) 

Ordenar filas

4.- arrange(): re-ordena las filas todas las columnas de un data frame

  • en orden ascendente (por defecto) o descendente con desc()
mypresidents %>% arrange(desc(duracion))

# ordenar por más de una columna: primero por duración, 
# en caso de empate por partido
mypresidents %>% arrange(desc(duracion), party) 

Resumir todo el conjunto de datos

5.- summarize(): colapsa valores de un data frame en una sola fila resumen

  • Especificando cómo se reducirá una columna entera de datos en un solo valor.
library(lubridate)
mypresidents %>%
  summarize(
    media_duracion = mean(duracion),
    N = n(),                        # n(): cuenta número de filas
    first_year = min(year(start)),  # year(): año de una fecha
    num_dems = sum(party == "Democratic") )
  • summarize() suele usarse en conjunción con group_by()

group_by()

  • group_by(): cambia el alcance de cada función para que no actúe sobre todo el data frame sino en grupos individuales

  • ¿Cuál es la duración media de los demócratas y de los republicanos? Hacerlo por separado no es eficiente: especificamos que las filas deben ser agrupadas

mypresidents %>% group_by(party) %>%         # solo "marca" dos grupos 
  summarize(N = n(),                         # nuevas variables
            media_duracion = mean(duracion)) # 
  • Nuevo conjunto de datos con nuevas variables (columnas) a un distinto nivel de observación (fila): una fila para cada valor del grupo
  • En Excel: Tablas dinámicas, AGRUPARPOR() (y SUMAR.SI/SUMIF)

  • ungroup() elimina la agrupación para volver a operar en datos desagrupados

mypresidents %>% group_by(party) %>% mutate(MD = mean(duracion)) %>% 
  ungroup() %>% arrange(duracion) %>%  slice_head(n=1)

Algunas funciones adicionales para columnas

  • select() para muchas variables usando : y - con nombres
library(nycflights13)           # incluye flights:  19 variables
select(flights, year:arr_time)  # desde variable "year" hasta "arr_time"
select(flights, -(year:day))    # todas menos "year, month, day"
  • También funciones como:

    • starts_with("abc"): nombres que comienzan con “abc”.
    • ends_with("xyz"): nombres que acaban con “xyz”.
    • contains("ijk"): nombres que contienen “ijk”.
    • num_range("x", 1:3): para x1, x2 y x3.
    • matches(): nombres que coinciden con una expresión regular

  • pull(var): extrae una única columna (como vector)
mypresidents %>% pull(duracion) %>% mean()

Algunas funciones adicionales para filas

  • slice(), slice_head(), slice_sample(): extraen filas por posición (en lugar de condición) o aleatoriamente
mypresidents %>% slice_head(n=3)
  • distinct(): extrae sólo las filas únicas (una o varias variables)
mypresidents %>% distinct(party)
  • drop_na() y replace_na(): elimina/reemplaza filas con valores ausentes

Algunas funciones adicionales para crear variables

  • count(): cuenta los valores únicos de una o más variables
mypresidents %>% count(party)    
# mypresidents %>% group_by(party) %>% summarize(n=n())
mypresidents %>% count(party, sort = TRUE)
  • across(): aplica la misma transformación a múltiples columnas
flights %>% mutate(across(air_time:distance, ~ log(.x)+1))
flights %>% mutate(across(where(is.character), ~ parse_factor(.x)))
  • Operadores aritméticos (+, -, *, /, ^, %/%, %%) y lógicos (<, <=, >, >=, !=)

  • Funciones como log(), lag(), lead(), cumsum(), row_number() etc.

Algunas funciones adicionales para crear variables (cont.)

  • Muchas funciones son equivalentes a otras de R base:

    • parse_number(), parse_factor(), etc. por as.number(), as.factor(), etc.

    • bind_cols() y bind_rows() por cbind() y rbind()

    • if_else(): ejecución condicional por ifelse() (también case_when())

flights %>% 
  mutate(retraso = if_else(dep_delay > 0, "tarde", "bien")) 
flights %>% 
  mutate(retraso = if_else(dep_delay > 0, "tarde",    # encadenados
                      if_else(dep_delay <0, "bien", "normal")))
  • Discretizar variables: cut_interval(), cut_number(), cut_width()

Algunas funciones adicionales para resúmenes

  • Medidas de centralidad y de dispersión: mean(x), median(x), sd(x), IQR(x)

  • Medidas de rango: min(x), quantile(x, 0.25), max(x)

  • Medidas de posición: first(x), nth(x, 2), last(x).

  • Sumas, productos, etc.

  • Conteos:

    • n(): observaciones totales (tamaño del grupo)

    • sum(!is.na(x)): observaciones no ausentes

    • n_distinct(x): filas distintas en x

Pivotar para ordenar

Cuatro representaciones de los mismos datos

library(tidyverse)
table1     # datos ordenados
table2     # varios valores por celda
table3     # más de una variable en una columna
table4a 
table4b
  • table4a y table4b ofrecen información útil para presentación

Mismos datos, dos formatos: ancho o largo

  • La utilidad de almacenar los datos en un rectángulo ancho (“wide”) o en uno largo (“long”) depende de qué queramos hacer

    • P.e., Excel prefiere el formato largo para tablas dinámicas, fórmulas de agregación (SUMAR.SI) y algunos gráficos

  • El cambio de forma entre formatos es una tarea habitual del analista de datos.

  • Cambiar entre representación larga y ancha se conoce como pivotar (o girar)

table4a        # formato ancho
table1         # formato largo

pivot_longer(): de ancho a largo

  • Pivotar las variables en dos nuevas columnas (deben crearse)
pivot_longer(table4a, 
             cols=2:3, 
             names_to = "year", 
             values_to = "cases") 
  1. data frame a cambiar de forma

  2. nombres o índices de las columnas que representan valores, no variables

  3. los nombres de esas antiguas variables van como valores a nueva variable

  4. los valores de las antiguas celdas van a otra nueva variable

pivot_longer(): de ancho a largo (cont.)

  • Recordad que existen formas equivalentes de hacer lo mismo
table4a %>% pivot_longer(cols = `1999`:`2000`, 
                         values_to = "cases", names_to = "year")
  • Notar que los nombres de columna son caracteres y cuando son números van entre ` (evita confusión con índice de posición)
  • Deberíamos cambiar el tipo de las nuevas variables
table4a %>% 
  pivot_longer(2:3, names_to = "year", values_to = "cases") %>%
  mutate(year= parse_number(year))

pivot_wider(): de largo a ancho

table2 %>%
    pivot_wider(names_from = type,  
                values_from = count)
  1. el data frame a cambiar de forma
  2. nombre de la variable de cuyos valores vienen los nuevos nombres de columnas
  3. nombre de la variable de la que tomar los valores para las nuevas columnas
table1 %>% select(-population) %>%            # Tabla de presentación 
    pivot_wider(names_from = year, values_from = cases)   

Dos funciones útiles

  • separate(): dividir una columna en múltiples variables indicando un separador o vector de posiciones en las que dividir
table3 %>% separate(rate, into = c("cases", "population"), sep = "/")
table3 %>% separate(year, into = c("century", "year"), sep = 2)
  • Con convert = TRUE intenta convertir el tipo (no mantener carácter)
table3 %>% separate(rate, into = c("cases", "population"), 
                    convert = TRUE)
  • unite(): combinar múltiples columnas en una
table5 %>% 
  unite(new, century, year, sep = "-")

Datos relacionales

Múltiples tablas de datos

  • Analizar datos suele implicar múltiples tablas

    • diferentes orígenes: ej., dptos. de empresa (personal, ventas, almacén)

    • almacenamiento más eficiente: elementos “similares” dentro de una tabla y diferentes entre ellas

  • Para poder combinar la información los datos deben ser relacionales: cada par de tablas están relacionadas mediante identificadores llamados claves

  • P.e., la biblioteca nycflights13 contiene varias tablas: el nombre de la compañía está “codificado” en flights y se puede encontrar en airlines
flights %>% select(dep_time,arr_time,carrier:dest) 
airlines
  • Ambas tablas contienen un identificador común clave (“key”): carrier

Relaciones entre tablas

Datos relacionales

  • Tipos de claves:

    • Primaria (o interna): identifican de forma única cada observación en una tabla. Puede ser una sola variable (en planes) o múltiples (en weather)

      • Subrogada = número de fila, si la tabla carece de identificación única
    • Secundaria (o externa): señala a la clave primaria de otra tabla

  • Una clave primaria y una externa (asociada) en otra tabla forman una relación:

    • de uno-a-muchos (ej., vuelos y aviones), de uno-a-uno, de muchos-a-muchos (ej., aerolíneas y aeropuertos), de muchos-a-uno
  • Operaciones que se pueden realizar con dos tablas: uniones de transformación, uniones de filtro y operaciones de conjunto

Uniones de transformación

  • Añaden nuevas variables a una tabla desde filas coincidentes en otra.

  • Ejemplo:

  • cbind() o bind_columns(): nuevas columnas para filas en el mismo orden
  • Dos argumentos obligatorios: las tablas que se unen
# queremos añadir el nombre de las compañias en la tabla de vuelos
flights2 <- flights %>% 
              select(year:day, hour, origin, dest, carrier, tailnum)
airlines

Argumento by: ¿Cómo se emparejan las tablas?

  • Las claves (variables que relacionan ambas tablas) se indican, para una variable, con by = "varX" o, para varias, con by = c("varX", "varY")
flights2 %>% inner_join(airlines, by = "carrier") 
flights2 %>% inner_join(weather, 
                by = c("year", "month", "day", "hour", "origin"))
  • Si se omite el argumento by, se usan todas las variables en común. Esto no siempre es deseable: ej., año no es lo mismo en flights y planes

  • Columnas con el mismo nombre (ej., año) se desambigúan con un sufijo

flights2 %>% left_join(planes, by = c("tailnum"))
  • by = c("x1" = "y1", "x2" = "y2") para emparejar la variable x1 en la primera tabla con la variable y1 en la segunda, y la variable x2 con y2
flights2 %>% left_join(airports, 
                  by = c("dest" = "faa"))    # aeropuerto de destino

Unión interna

df1 <- tibble(clave = c(1:3), val_x = c("x1", "x2", "x3"))
df2 <- tibble(clave = c(1:2, 4), val_y = c("y1","y2","y4"))
  • inner_join(x, y) sólo incluye observaciones que coincidan en x y y.
df1 %>% inner_join(df2, by = "clave")

Uniones externas

  • Cuando una fila no coincide en una unión externa, las nuevas variables se rellenan como valores ausentes

  • left_join(df1, df2): mantiene todas las observaciones en x, coincidan o no con la de y

    • (no se pierden observaciones de la tabla primaria)
  • right_join(df1, df2): mantiene todas las observaciones en y

  • full_join(df1, df2): incluye todas las observaciones de x e y

Claves duplicadas

  • Si una coincidencia no es única, se generan todas las combinaciones posibles (producto cartesiano) de las observaciones coincidentes
  • En una tabla: añade información en una relación de uno a muchos.
  • En ambas tablas: igualmente, todas las combinaciones posibles
    • posible error: NO hay clave primaria única

Uniones de filtro

  • Filtra las observaciones de la tabla de la izquierda basándose en si coinciden o no con una observación de la otra tabla

  • Se tiene un subconjunto de las filas de la tabla de la izquierda

  • semi_join(x, y) mantiene las observaciones en x que están en y

df1 %>% semi_join(df2, 
          by = "clave")
  • anti_join(x, y) elimina las observaciones en x que están en y

df1 %>% anti_join(df2, 
          by = "clave")

Uniones de filtro (cont.)

  • Claves duplicadas: en uniones de filtro sólo importa la existencia de una coincidencia, NO qué observación coincida \(\Rightarrow\) NUNCA duplica filas
  • Las uniones de filtro son útiles para diagnosticar desajustes de uniones (qué observaciones serán emparejadas)
flights %>% anti_join(planes, by = "tailnum") %>%   # vuelos sin información del avión
              count(tailnum, sort = TRUE)
  • Pueden ser equivalentes a usar filter(), con tablas previamente resumidas, pero permiten filtrados complejos fácilmente

Operaciones de conjunto

  • Trabajan con filas completas, comparando valores de cada variable.

  • Esperan que x e y tengan las mismas variables, y tratan las observaciones (filas) como elementos de un conjunto.

  • Útil cuando se quiere dividir un filtro complejo en piezas más simples.

df1 <- tibble(x = 1:2, y = c(1, 1))
df2 <- tibble(x = c(1,1), y = 1:2)

intersect(df1, df2)     # solo filas tanto en df1 como en df2
union(df1, df2)         # filas únicas en ambas tablas df1 y df2` 
union_all(df1, df2)     # todas las filas de df1 y df2, manteniendo duplicados 
setdiff(df1, df2)       # filas en df1, pero no en df2
setdiff(df2, df1)